-- Copyright (c) 2024 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local class = require 'mc.class'
local log = require 'mc.logging'
local file_sec = require 'utils.file'
local utils = require 'mc.utils'
local bios_enum = require 'domain.bios_firmware.defs'
local vos = require 'utils.vos'
local fructl_handler = require 'infrastructure.fructl'
local firmware_upgrade = require 'infrastructure.firmware_upgrade'
local skynet = require 'skynet'
local prop_def = require 'macros.property_def'
local worker = require 'worker.core'
local utils_core = require 'utils.core'
local bios_flash = require 'libmgmt_protocol.bios.domain.pfr.flash'
local bios_factory = require 'factory.bios_factory'
local pro_global = require 'macros.property_global'
local client = require 'bios.client'
local context = require 'mc.context'
local obj_def = require 'macros.object_def'

local GoldPackage = class()

local SECURE_BOOT_PATH<const>   = '/bmc/kepler/Managers/1/SecureBoot'
local PFR_TMP_DIR<const>        = '/data/backup/bios/tmp'
local PFR_GOLD_DIR<const>       = '/data/backup/bios/gold'
local BIOS_HPM<const>           = 'bios.hpm'
local OLD_PFR_GOLD_PATH<const>  = '/data/backup/bios/gold/bios.hpm'

local VERIFY_TIMES<const> = 3
local ROOT_USER_GID<const> = 0 -- root用户组id
local GOLD_VALID<const> = 1
local GOLD_INVALID<const> = 0
local RET_OK<const> = 0
local FORCE_OFF_TIMES<const> = 30
local WAIT_OFF_TIMES<const> = 60
local POWER_ON_TIMES<const> = 20

function GoldPackage:ctor(system_id)
    self.SystemId               = system_id
    self.FwVerifyResult         = bios_enum.PfrVerify.Success -- 当前升级包的校验结果
    self.BackupFwVerifyResult   = bios_enum.PfrVerify.Success -- 起来之后，备份包的校验结果
    self.secure_boot_info       = false
    if self.SystemId == bios_enum.SINGLE_HOST_SYSTEM_ID then
        self.PfrTmpDir = PFR_TMP_DIR
        self.PfrGoldDir = PFR_GOLD_DIR
    else
        self.PfrTmpDir = string.format('%s/%s', PFR_TMP_DIR, system_id)
        self.PfrGoldDir = string.format('%s/%s', PFR_GOLD_DIR, system_id)
    end
    self.PfrBiosTmpFile = string.format('%s/%s', self.PfrTmpDir, BIOS_HPM)
    self.PfrBiosGoldFile = string.format('%s/%s', self.PfrGoldDir, BIOS_HPM)
    utils.mkdir_with_parents(self.PfrTmpDir, utils.S_IRWXU | utils.S_IRGRP | utils.S_IXGRP)
    utils.mkdir_with_parents(self.PfrGoldDir, utils.S_IRWXU | utils.S_IRGRP | utils.S_IXGRP)
end

function GoldPackage:set_prop(prop, val)
    self[prop] = val
end

function GoldPackage:set_verify_res(prop, val)
    self[prop] = val
    local cfg = bios_factory.get_service('bios_service')
    if self.FwVerifyResult == bios_enum.PfrVerify.Failed and
        self.BackupFwVerifyResult == bios_enum.PfrVerify.Failed then
        log:error('[bios]alert secure boot failed.')
        cfg:set_prop('RecoverFailed', bios_enum.PfrVerify.Failed, self.system_id)
    else
        log:notice('[bios]secure boot success.')
        cfg:set_prop('RecoverFailed', bios_enum.PfrVerify.Success, self.system_id)
    end
end

function GoldPackage:get_prop(prop)
    return self[prop]
end

function GoldPackage:cache_hpm(hpm_path)
    local bios_ser = bios_factory.get_service('bios_service')
    local flash_channel_ids = bios_ser:get_prop(obj_def.FLASH_CHANNEL_IDS, self.SystemId) or {}
    if #flash_channel_ids == 0 and self.SystemId ~= bios_enum.SINGLE_HOST_SYSTEM_ID then
        log:notice('[bios]gold package: cache_hpm return, system id is %s', self.SystemId)
        return
    end

    if not hpm_path or not vos.get_file_accessible(hpm_path)  then
        error('[bios]gold package: hpm path is no accessible')
    end

    -- PFR:缓存hpm包
    local tmp_path = self.PfrBiosTmpFile
    if vos.get_file_accessible(tmp_path) then
        utils.remove_file(tmp_path)
    end

    utils.mkdir_with_parents(self.PfrTmpDir, utils.S_IRWXU | utils.S_IRGRP | utils.S_IXGRP)
    local backup_res
    for _ = 1, 3 do
        backup_res = file_sec.copy_file_s(hpm_path, tmp_path)
        if backup_res == RET_OK then
            break
        end
    end

    if backup_res ~= RET_OK then
        error(string.format('[bios]gold package: cache back up hpm fail, system id is %s', self.SystemId))
    end
    utils_core.chmod_s(tmp_path, utils.S_IRUSR) -- 修改权限为400
    log:notice('[bios]gold package: backup tmp path success, system id is %s', self.SystemId)
end

function GoldPackage:backup_hpm()
    local tmp_path = self.PfrBiosTmpFile
    local pfr_path = self.PfrBiosGoldFile
    local work = worker.new(0)
    if not vos.get_file_accessible(tmp_path) then
        log:notice("[bios]gold package: back_up_hpm, tmp path no access, system id is %s", self.SystemId)
        return true
    end

    utils.mkdir_with_parents(self.PfrGoldDir, utils.S_IRWXU | utils.S_IRGRP | utils.S_IXGRP)

    if vos.get_file_accessible(pfr_path) then
        utils.remove_file(pfr_path)
    end

    work:start(string.format([[
        local file_sec = require 'utils.file'
        package_backup_res = -1
        for _ = 1, 3 do
            package_backup_res = file_sec.copy_file_s('%s', '%s')
            if package_backup_res == 0 then
                break
            end
        end
    ]], tmp_path, pfr_path))

    local time_out_count = 0
    while not work:is_complete() and time_out_count <= 50 do
        time_out_count = time_out_count + 1
        skynet.sleep(100)
    end

    local backup_res = tonumber(work:get_global('package_backup_res'))
    skynet.sleep(100)
    utils.remove_file(tmp_path)
    if backup_res ~= 0 or time_out_count > 50 then
        self:set_verify_res('BackupFwVerifyResult', bios_enum.PfrVerify.Failed)
        log:error('[bios]gold package: back_up_hpm, backup hpm fail, system id is %s.', self.SystemId)
        utils.remove_file(pfr_path)
        return false
    end
    utils_core.chown_s(pfr_path, ROOT_USER_GID, ROOT_USER_GID)
    utils_core.chmod_s(pfr_path, utils.S_IRUSR) -- 修改权限为400
    log:notice('[bios]gold package: back_up_hpm, backup gold hpm success, system id is %s.', self.SystemId)
    self:set_verify_res('BackupFwVerifyResult', bios_enum.PfrVerify.Success)
    return true
end

function GoldPackage:set_verify_fail()
    if self.FwVerifyResult == bios_enum.PfrVerify.Success then
        log:notice('[GoldPackage]gold package: system %s set FwVerifyResult failed', self.SystemId)
        self:set_verify_res('FwVerifyResult', bios_enum.PfrVerify.Failed)
    else
        log:notice('[GoldPackage]gold package: system %s set BackupFwVerifyResult failed', self.SystemId)
        self:set_verify_res('BackupFwVerifyResult', bios_enum.PfrVerify.Failed)
    end
end

function GoldPackage:_confirm_power_off()
    fructl_handler.try_set_power_state(FORCE_OFF_TIMES, 'ForceOff', 'Unknown', self.SystemId)
    fructl_handler.wait_until_power_off(WAIT_OFF_TIMES, self.SystemId)
end

function GoldPackage:upgrade_post(valid_path)
    -- 升级之后需要把/data/backup/bios/tmp删除掉（可能是有问题的包）
    if vos.get_file_accessible(self.PfrBiosTmpFile) then
        utils.remove_file(self.PfrBiosTmpFile)
    end
    if vos.get_file_accessible(valid_path) then
        utils.remove_file(valid_path)
    end
end

function GoldPackage:recover_from_gold()
    -- 自愈
    local ok, err = pcall(function()
        self:_confirm_power_off()
    end)
    if not ok then
        log:error('[GoldPackage]GoldPackage: system %s confirm power off fail, err = %s', self.SystemId, err)
        return false
    end
    log:notice('[GoldPackage]GoldPackage: system %s confirm power off, start recover.', self.SystemId)
    local verify_res = self.BackupFwVerifyResult
    local hpm_path =  self.PfrBiosGoldFile
    if verify_res == bios_enum.PfrVerify.Failed then
        log:error('[GoldPackage]gold package: recover_from_gold fail, cause backup verify fail, system id is %s.',
            self.SystemId)
        return false
    end
    if not vos.get_file_accessible(hpm_path) then
        -- glod不存在直接返回检测不通过
        self:set_verify_fail()
        log:error('[GoldPackage]gold package: recover_from_gold fail, cause hpm is not exist, system id is %s.',
            self.SystemId)
        return false
    end

    local valid_path = firmware_upgrade.get_package_info(hpm_path)
    if not valid_path then
        log:error('[GoldPackage]gold package: system %s recover_from_gold fail, cause valid path is nil', self.SystemId)
        return false
    end
    local upgrade_ser = bios_factory.get_service('upgrade_service')
    upgrade_ser:inner_upgrade(valid_path)
    self:upgrade_post(valid_path)
    return true
end

function GoldPackage:get_secure_boot_info(flash)
    local ok, secure_boot_info = pcall(flash.get_secure_boot_info, flash)
    if ok and secure_boot_info then
        log:notice('system %s secure_boot_info: %s', self.SystemId, secure_boot_info)
        self.secure_boot_info = secure_boot_info
    end
end

function GoldPackage:verify()
    local bios_ser = bios_factory.get_service('bios_service')
    local flash_size = bios_ser:get_prop(obj_def.FLASH_SIZE, bios_enum.SINGLE_HOST_SYSTEM_ID)
    local flash = bios_flash.new(bios_enum.PackagePeriod.Period3)
    local ok, err = pcall(flash.verify, flash, pro_global.G_BIOS_FIRMWARE_CUSTOM_CONFIG, client,
        client.PProxyDriverInsmod, client.PFileFileChown, client.PProxyDriverRmmod, flash_size)
    if ok then
        self:get_secure_boot_info(flash)
        self:set_verify_res('FwVerifyResult', bios_enum.PfrVerify.Success)
        return
    end
    self:set_verify_fail()
    error(string.format('[GoldPackage]GoldPackage: verify fail, err(%s)', err))
end

-- 检查安全启动状态、自愈
function GoldPackage:secure_boot_verify_and_recover()
    -- 自愈
    local ok, err = pcall(GoldPackage.recover_from_gold, self)
    if not ok then
        log:error('[GoldPackage]GoldPackage: upgrade from gold fail, err (%s)', err)
        return false
    else
        log:notice('[GoldPackage]GoldPackage: upgrade from gold success')
    end
    ok, err = pcall(function()
        return fructl_handler.try_set_power_state(POWER_ON_TIMES, 'On', 'Unknown', self.SystemId)
    end)
    if not ok then
        log:error('[GoldPackage]GoldPackage: power on after recover failed , err (%s)', err)
        return false
    end
    return true
end

local function get_secure_boot_object()
    local ok, obj = pcall(function ()
        return client:GetSecureBootObjects()[SECURE_BOOT_PATH]
    end)
    if not ok then
        log:error('get secure boot obj failed')
        return
    end

    return obj
end

function GoldPackage:set_secure_boot_info()
    local obj = get_secure_boot_object()
    if not obj or not self.secure_boot_info then
        return
    end

    local ok, ret = pcall(function ()
        obj:SetBiosSignature(context.new(), self.SystemId, self.secure_boot_info)
    end)

    if not ok then
        log:error('system %s set secure boot info failed, err: %s', ret, self.SystemId)
    end
end

-- 校验、自愈
function GoldPackage:verify_and_recover()
    -- 校验
    for i = 1, VERIFY_TIMES do
        log:notice('[GoldPackage]GoldPackage:system %s start verify %d times.', self.SystemId, i)
        local ok, err = pcall(function()
            self:verify()
        end)
        if ok then
            self:set_secure_boot_info()
            log:notice('[GoldPackage]GoldPackage:system %s verify success', self.SystemId)
            return true
        end
        -- 自愈
        ok, err = pcall(GoldPackage.recover_from_gold, self)
        if ok then
            log:notice('[GoldPackage]GoldPackage: system %s upgrade from gold success, continue verify', self.SystemId)
        else
            log:error('[GoldPackage]GoldPackage: system %s upgrade from gold fail, err (%s)', err, self.SystemId)
        end
    end
    return false
end

function GoldPackage:get_bios_gold_valid()
    if vos.get_file_accessible(self.PfrBiosGoldFile) and
        self.BackupFwVerifyResult == prop_def.VERIFY_RESULT_SUCCESS then
        return GOLD_VALID
    end
    return GOLD_INVALID
end

function GoldPackage:remove_tmp_hpm()
    local tmp_path = self.PfrBiosTmpFile
    if vos.get_file_accessible(tmp_path) then
        utils.remove_file(tmp_path)
    end
end

return GoldPackage